/**
 * Copyright (c) 2008, EffCode, www.effcode.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer. 
 * Redistributions in binary form must reproduce the above copyright notice, this list 
 * of conditions and the following disclaimer in the documentation and/or other materials
 * provided with the distribution. 
 * Neither the name of the EffCode nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior written 
 * permission. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.effdom.impl;

import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import org.effdom.Attr;
import org.effdom.Document;
import org.effdom.Element;
import org.effdom.io.impl.DocParserImpl;

/**
 * @author <a href="mailto:mattias@effcode.com">Mattias Jonsson</a>
 */
public class ElementImpl extends NodeBase implements Element, Serializable {

    private static final long serialVersionUID = -3957906897385578341L;

    private final LinkedHashMap<Short, LinkedList<Element>> elemMap = new LinkedHashMap<Short, LinkedList<Element>>();

    private final LinkedHashMap<Short, Attr> attrMap = new LinkedHashMap<Short, Attr>();

    public ElementImpl(Document document, Element parent, short id) {
        super(document, parent, id);
    }

    public Collection<Attr> attrs() {
        return attrMap.values();
    }

    public Attr getAttr(short id) {
        return attrMap.get(id);
    }

    public String attrValue(short id) {
        try {
            return getAttr(id).value();
        } catch (NullPointerException e) {
            return null;
        }
    }

    public byte[] attrValueAsByteArray(short id) throws IllegalStateException {
        try {
            return getAttr(id).valueAsByteArray();
        } catch (NullPointerException e) {
            return null;
        }
    }

    public int attrValueAsInt(short id) throws IllegalStateException {
        try {
            return getAttr(id).valueAsInt();
        } catch (NullPointerException e) {
            return 0;
        }
    }

    public long attrValueAsLong(short id) throws IllegalStateException {
        try {
            return getAttr(id).valueAsLong();
        } catch (NullPointerException e) {
            return 0;
        }
    }

    public Attr removeAttr(short id) {
        return attrMap.remove(id);
    }

    public Element firstElement(short id) {
        LinkedList<Element> list = elemMap.get(id);
        Element result = null;
        if (list != null && list.size() > 0) {
            result = list.element();
        }
        return result;
    }

    public List<Element> elements(short id) {
        List<Element> result = elemMap.get(id);
        if (result == null) {
            result = new LinkedList<Element>();
        }
        return result;
    }

    public boolean hasAttr(short id) {
        boolean result = false;
        if (attrMap.get(id) != null) {
            result = true;
        }
        return result;
    }

    public List<Element> elements() {
        List<Element> result = new LinkedList<Element>();
        for (LinkedList<Element> list : elemMap.values()) {
            for (Element elem : list) {
                result.add(elem);
            }
        }
        return result;
    }

    Attr setAttr(Attr attr) {
        ((NodeBase) attr).setParent(this);
        return attrMap.put(attr.id(), attr);
    }

    public Element attr(short id, byte[] value) {
        Attr attr = new AttrBinary(doc, this, id, value);
        setAttr(attr);
        return this;
    }

    public Element attr(short id, int value) throws IllegalStateException {
        Attr attr = new AttrNumber(doc, this, id, value);
        setAttr(attr);
        return this;
    }

    public Element attr(short id, long value) throws IllegalStateException {
        Attr attr = new AttrNumber(doc, this, id, value);
        setAttr(attr);
        return this;
    }

    public Element attr(short id, String value) throws IllegalStateException {
        Attr attr = new AttrString(doc, this, id, value);
        setAttr(attr);
        return this;
    }

    public boolean hasElements() {
        boolean result = false;
        if (elemMap.size() > 0) {
            result = true;
        }
        return result;
    }

    public int hasElements(short id) {
        return elements(id).size();
    }

    public boolean hasAttr() {
        boolean result = false;
        if (attrMap.size() > 0) {
            result = true;
        }
        return result;
    }

    public Element removeElement(Element element) throws IllegalStateException {
        if (element instanceof ElementImpl) {
            LinkedList<Element> list = elemMap.get(element.id());
            if (list != null) {
                list.remove(element);
                if (list.size() > 0) {
                    elemMap.put(element.id(), list);
                } else {
                    elemMap.remove(element.id());
                }
            }
        } else {
            throw new IllegalStateException("Node " + element.getClass().getName() + " not supported");
        }
        return element;
    }

    public Element firstElement() throws IllegalStateException {
        Element result = null;
        if (elemMap.size() > 0) {
            try {
                result = elemMap.values().iterator().next().getFirst();
            } catch (Throwable e) {
                // Lazy catch
            }
        }
        return result;
    }

    private void appendElement(Element element) throws IllegalStateException {
        if (element instanceof ElementImpl) {
            LinkedList<Element> list = elemMap.get(element.id());
            if (list == null) {
                list = new LinkedList<Element>();
            }
            list.add(element);
            elemMap.put(element.id(), list);
        } else {
            throw new IllegalStateException("Unsupported Element implemenation");
        }
    }

    public Element element(short id) throws IllegalStateException {
        Element elem = doc.createElement(this, id);
        appendElement(elem);
        return elem;
    }
    
    public Element element(Element elem) throws IllegalStateException {
        appendElement(elem);
        return this;
    }

    public Element parent() throws IllegalStateException {
        if (parent == null) {
            throw new IllegalStateException("Top is reached");
        }
        return parent;
    }

    public void addLength(Counter counter) {
        int length = length();
        if(length <= Byte.MAX_VALUE) {
            length += DocParserImpl.SIZE_LENGTH;
        } else {
            length += DocParserImpl.SIZE_LENGTH_L;
        }
        length += DocParserImpl.ELEM_HEADER_SIZE;
        if(id >= Byte.MIN_VALUE && id <= Byte.MAX_VALUE) {
            length += DocParserImpl.ID_LENGTH;
        } else {
            length += DocParserImpl.ID_LENGTH_L;
        }
        counter.addLength(length);
    }

    public int length() {
        Counter counter = new Counter();
        calcLength(counter);
        return counter.getLength();
    }

    private void calcLength(Counter counter) {
        if (hasAttr()) {
            for (Attr attr : attrMap.values()) {
                ((NodeBase) attr).addLength(counter);
            }
        }
        if (hasElements()) {
            for (Element elem : elements()) {
                ((NodeBase) elem).addLength(counter);
            }
        }
    }

    public String toString() {
        StringBuilder buf = new StringBuilder();
        toString(buf, 0);
        return new String(buf);
    }

    void toString(StringBuilder buf, int index) {
        buf.append("<" + String.valueOf(id()));
        if (hasAttr()) {
            for (Attr attr : attrMap.values()) {
                buf.append(" " + attr);
            }
        }
        if (hasElements()) {
            buf.append(" >");
            buf.append("\n");
            for (Element elem : elements()) {
                for (int j = 0; j < index + 1; j++) {
                    buf.append("  ");
                }
                ((ElementImpl) elem).toString(buf, index + 1);
                buf.append("\n");
            }
            for (int j = 0; j < index; j++) {
                buf.append("  ");
            }
        } else {
            buf.append(">");
        }
        buf.append("</" + String.valueOf(id()) + ">");
    }
}
